﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Windows.Foundation;
using Windows.UI;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Media;
using Windows.UI.Xaml.Shapes;

namespace RayGraphComponent
{
    public sealed partial class LineGraph : Canvas
    {
        private double drawAreaBottom, drawAreaLeft, divisionX, divisionY;

        private double seriesMinX, seriesMaxX, seriesMinY, seriesMaxY;

        private bool loaded;

        private IList<Serie> series;

        private Queue<Brush> colors;

        private IList<UIElement> xDataMarkers, yDataMarkers;

        private UIElement xGuideLines, yGuideLines;

        private enum GuideLines
        {
            All,
            X,
            Y
        }

        public LineGraph()
            : base()
        {
            this.colors = new Queue<Brush>();
            this.colors.Enqueue(new SolidColorBrush(Colors.Red));
            this.colors.Enqueue(new SolidColorBrush(Colors.Blue));
            this.colors.Enqueue(new SolidColorBrush(Colors.Green));
            this.colors.Enqueue(new SolidColorBrush(Colors.Yellow));
            this.colors.Enqueue(new SolidColorBrush(Colors.Purple));

            this.xDataMarkers = new List<UIElement>();
            this.yDataMarkers = new List<UIElement>();

            this.series = new List<Serie>();

            this.GuideLineColor = new SolidColorBrush(Colors.LightGray);
            this.GuideTextColor = new SolidColorBrush(Colors.Black);

            this.XGuideLineResolution = 100;
            this.YGuideLineResolution = 100;

            this.GuideTextSize = 11;

            this.GuideLineThickness = 1;

            this.DrawAreaHorizontalOffset = 40;
            this.DrawAreaVerticalOffset = 40;

            this.loaded = false;

            this.AutoRedraw = true;

            this.Loaded += delegate
            {
                loaded = true;
                this.Draw();
            };
        }

        public void AddSerie(Serie s)
        {
            if (s != null)
            {
                s.Color = colors.Dequeue();
                series.Add(s);
                s.DataUpdated += new RoutedEventHandler(SerieDataUpdated);

                if (this.loaded)
                {
                    this.Draw();
                }
            }
        }

        private void SerieDataUpdated(Object sender, RoutedEventArgs e)
        {
            Serie s = sender as Serie; // Serie sender says it has updated

            if (this.AutoRedraw)
            {
                this.Draw();
            }
        }

        private void Draw()
        {
            bool dataIsAvailable = false;

            this.seriesMinY = Double.NaN;
            this.seriesMaxY = Double.NaN;
            this.seriesMinX = Double.NaN;
            this.seriesMaxX = Double.NaN;

            // Get and set the MIN and MAX of all series
            foreach (Serie serie in series)
            {
                if (serie.Data.Count() > 1)
                {
                    dataIsAvailable = true;
                }

                if (Double.IsNaN(this.seriesMinY) || serie.MinY < this.seriesMinY)
                {
                    this.seriesMinY = serie.MinY;
                }

                if (Double.IsNaN(this.seriesMaxY) || serie.MaxY > this.seriesMaxY)
                {
                    this.seriesMaxY = serie.MaxY;
                }

                if (Double.IsNaN(this.seriesMinX) || serie.MinX < this.seriesMinX)
                {
                    this.seriesMinX = serie.MinX;
                }

                if (Double.IsNaN(this.seriesMaxX) || serie.MaxX > this.seriesMaxX)
                {
                    this.seriesMaxX = serie.MaxX;
                }
            }

            if (dataIsAvailable)
            {
                foreach (Serie serie in series)
                {
                    if (serie.Data.Count() > 1)
                    {
                        this.DrawLine(serie);
                    }
                }

                if (this.DrawXGuideLines)
                {
                    this.DrawXLines();
                }

                if (this.DrawYGuideLines)
                {
                    this.DrawYLines();
                }
            }
        }

        private void DrawLine(Serie serie)
        {
            //this.InvalidateMeasure(); not sure why anymore...

            this.RemoveLine(serie);

            double height = this.ActualHeight - (2 * this.DrawAreaVerticalOffset);
            double width = this.ActualWidth - (2 * this.DrawAreaHorizontalOffset);

            this.drawAreaBottom = 0 + height + this.DrawAreaVerticalOffset; // bottom left of drawing area, add 1 so the offset gets added below
            this.drawAreaLeft = 0 + this.DrawAreaHorizontalOffset; // left of drawing area, add 1 so the offset gets added left

            this.divisionX = (width / (this.seriesMaxX - this.seriesMinX));
            Debug.WriteLine("{0} : {1} / ({2} - {3}) = {4}", this.Name, width, this.seriesMaxX, this.seriesMinX, divisionX);
            this.divisionY = (height / (this.seriesMaxY - this.seriesMinY));

            Path myPath = new Path();
            PathFigure myPathFigure = new PathFigure();
            PathSegmentCollection myPathSegmentCollection = new PathSegmentCollection();
            PathFigureCollection myPathFigureCollection = new PathFigureCollection();
            PathGeometry myPathGeometry = new PathGeometry();

            Point p;
            // prepare for drawing by modifying y value
            for (int i = 0; i < serie.Data.Count; i++) // for every datapoint in the serie
            {
                p = serie.Data[i];
                p.X = this.drawAreaLeft + (divisionX * i);
                p.Y = drawAreaBottom - ((p.Y - seriesMinY) * divisionY);
                // data[i] = p; NO - Keep serie data unchanged

                Debug.WriteLine("{0} : ({1}) * {2} = {3}", this.Name, serie.Data[i], i, p.X);

                if (i == 0)
                {
                    myPathFigure.StartPoint = p;
                }
                else
                {
                    LineSegment segment = new LineSegment();
                    segment.Point = p;

                    myPathSegmentCollection.Add(segment);
                }
            }

            if (this.Fill) // not fully tested
            {
                p.Y = drawAreaBottom - 0;
                LineSegment Endsegment = new LineSegment();
                Endsegment.Point = p;
                myPathSegmentCollection.Add(Endsegment);
            }

            myPathFigure.Segments = myPathSegmentCollection;
            myPathFigureCollection.Add(myPathFigure);
            myPathGeometry.Figures = myPathFigureCollection;

            myPath.Stroke = serie.Color;
            myPath.StrokeThickness = 2;
            if (this.Fill)
            {
                myPath.Fill = serie.Color;
            }

            myPath.Data = myPathGeometry;

            Canvas.SetZIndex(myPath, 10);

            this.Children.Add(myPath);
            serie.UIElement = myPath;
        }

        private void DrawXLines()
        {
            this.RemoveGuidlines(GuideLines.X);

            double width = this.ActualWidth; // just in case it changes?
            double height = this.ActualHeight;

            double dpp = (width - (2 * this.DrawAreaHorizontalOffset)) / (seriesMaxX - seriesMinX); // value of data per point

            int loops = (int)width / this.XGuideLineResolution; // amount of x info

            /*
            if ((width / this.data.Count) > this.xResolution) // if there are less data points than the resolution, show the absolute data points
            {
                loops = this.data.Count;
            }
            */

            for (int i = 1; i < loops; i++)
            {
                double x = width / (loops - 1) * i;

                // text
                double length = seriesMaxX - seriesMinX;
                double interval = length / (loops - 1);
                //

                TextBlock tb = new TextBlock();
                tb.Text = Math.Round(seriesMinX + (i * interval), 1).ToString();
                tb.Foreground = this.GuideTextColor;
                tb.FontSize = this.GuideTextSize;
                tb.Margin = new Thickness(x - (tb.Text.Length / 2 * 6), height - 20, 0, 20);

                Path p = new Path();
                LineGeometry geo = new LineGeometry();
                geo.StartPoint = new Point(x, 20);
                geo.EndPoint = new Point(x, this.ActualHeight - 20);
                p.Stroke = this.GuideLineColor;
                p.StrokeDashArray = new DoubleCollection() { 5, 20 };
                p.StrokeThickness = this.GuideLineThickness;
                p.Data = geo;

                xDataMarkers.Add(tb);
                xGuideLines = p;

                Canvas.SetZIndex(tb, 15);
                Canvas.SetZIndex(p, 14);

                this.Children.Add(tb);
                this.Children.Add(p);
            }
        }

        private void DrawYLines()
        {
            RemoveGuidlines(GuideLines.Y);

            double height = this.ActualHeight;
            int loops = (int)height / this.YGuideLineResolution; // amount of Y info

            double dpp = (height - (2 * this.DrawAreaVerticalOffset)) / (seriesMaxY - seriesMinY); // value of data per point

            for (int i = 1; i < loops; i++)
            {
                // text
                double length = seriesMaxY - seriesMinY;
                double interval = length / (loops - 1);
                //

                TextBlock tb = new TextBlock();
                tb.Text = Math.Round(seriesMinY + (i * interval), 1).ToString();
                tb.Foreground = this.GuideTextColor;
                tb.FontSize = this.GuideTextSize;
                tb.Margin = new Thickness(5, drawAreaBottom - (i * (this.ActualHeight / 6)) - 5, 0, 0);

                Path p = new Path();
                LineGeometry geo = new LineGeometry();
                geo.StartPoint = new Point(25, drawAreaBottom - (i * (this.ActualHeight / 6)));
                geo.EndPoint = new Point(this.ActualWidth - 25, drawAreaBottom - (i * (this.ActualHeight / 6)));
                p.Stroke = this.GuideLineColor;
                p.StrokeDashArray = new DoubleCollection() { 5, 20 };
                p.StrokeThickness = this.GuideLineThickness;
                p.Data = geo;

                yDataMarkers.Add(tb);
                yGuideLines = p;

                Canvas.SetZIndex(tb, 15);
                Canvas.SetZIndex(p, 14);

                this.Children.Add(tb);
                this.Children.Add(p);
            }
        }

        public void RemoveSerie(Serie s)
        {
            this.RemoveLine(s);
            this.series.Remove(s);
        }

        private void RemoveLine(Serie s)
        {
            this.Children.Remove(s.UIElement);
        }

        private void RemoveGuidlines(GuideLines gl)
        {
            switch (gl)
            {
                case GuideLines.X:
                    {
                        this.Children.Remove(xGuideLines);

                        foreach (UIElement tb in xDataMarkers)
                        {
                            this.Children.Remove(tb);
                        }

                        xDataMarkers.Clear();

                        break;
                    }
                case GuideLines.Y:
                    {
                        this.Children.Remove(yGuideLines);

                        foreach (UIElement tb in yDataMarkers)
                        {
                            this.Children.Remove(tb);
                        }

                        yDataMarkers.Clear();

                        break;
                    }
                default:
                    {
                        this.RemoveGuidlines(GuideLines.X);
                        this.RemoveGuidlines(GuideLines.Y);
                        break;
                    }
            }
        }

        public void Redraw()
        {
            this.Draw();
        }
    }
}
